package framework
{
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IEventDispatcher;

    import mx.events.PropertyChangeEvent;
    import mx.events.PropertyChangeEventKind;

    import org.as3commons.lang.ClassUtils;
    import org.as3commons.lang.ObjectUtils;
    import org.as3commons.reflect.Accessor;
    import org.as3commons.reflect.Field;
    import org.as3commons.reflect.Method;
    import org.as3commons.reflect.Parameter;
    import org.as3commons.reflect.Type;

    internal class CtxCore extends EventDispatcher implements ICtx
    {
        public static var excludedProperties:Array = [ "$x", "$y" ];

        public function CtxCore()
        {
            super(null);
            addEventListener(CommandEvent.COMMAND, commandHandler);
        }

        public var beanClasses:Object = {};

        public var beans:Object = {};

        public function deleteBean(id:String):Boolean
        {
            var deleted:Boolean = hasBean(id);
            if (deleted)
            {
                var aopEvent:AOPEvent = new AOPEvent(AOPEvent.BEAN_DELETE, false, true);
                aopEvent.beanId = id;
                dispatchEvent(aopEvent);
                var pce:PropertyChangeEvent = new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, PropertyChangeEventKind.DELETE, id);
                dispatchEvent(pce);
                if (!aopEvent.isDefaultPrevented())
                {
                    delete beanClasses[id];
                    if (beans[id])
                        destroy(beans[id]);
                    delete beans[id];
                }
                else
                {
                    deleted = false;
                }
            }
            return deleted;
        }

        private function commandHandler(e:CommandEvent):void
        {
            var method:String = e.method ? e.method : "handle";
            var args:Array = e.args != null ? e.args : [ e ];
            var bean:* = getBean(e.beanId);
            var type:Type = Type.forInstance(bean);
            var handleMethod:Method = type.getMethod(method);
            if (handleMethod.parameters.length == 0)
            {
                handleMethod.invoke(this, null);
            }
            else
            {
                handleMethod.invoke(this, args);
            }
        }

        public function destroy(instance:*):*
        {
            if (isSimpleOrSpecial(instance))
                return instance;
            var type:Type = Type.forInstance(instance);
            var initBean:Method = type.getMethod("destroyBean");
            if (initBean)
            {
                initBean.invoke(instance, null);
            }
            if (instance is IEventDispatcher)
            {
                IEventDispatcher(instance).dispatchEvent(new Event(Event.REMOVED));
            }
            return instance;
        }

        public function getBean(id:String):*
        {
            var aopEvent:AOPEvent = new AOPEvent(AOPEvent.BEAN_ACCESS);
            aopEvent.beanId = id;
            dispatchEvent(aopEvent);
            var instance:* = beans[id];
            if (instance)
            {
                return wire(instance);
            }
            else
            {
                var clazz:Class = beanClasses[id];
                if (!clazz)
                {
                    return undefined;
                }
                delete beanClasses[id];
                return registerInstance(new clazz, id);
            }
        }

        public function getBeanId(bean:*):String
        {
            for (var k:String in beans)
            {
                if (bean == beans[k])
                    return k;
            }
            return null;
        }

        public function getBeansByType(clazz:Class):Array
        {
            var r:Array = [];
            var prop:String
            for (prop in beans)
            {
                if (beans[prop] is clazz)
                {
                    r.push(prop);
                }
            }
            for (prop in beanClasses)
            {
                if (clazz == beanClasses[prop])
                {
                    r.push(prop);
                }
            }
            return r;
        }

        public function init(instance:*, id:String):*
        {
            if (isSimpleOrSpecial(instance))
                return instance;
            var type:Type = Type.forInstance(instance);
            var initBean:Method = type.getMethod("initBean");
            if (initBean)
            {
                var args:Array = [];
                if (initBean.parameters.length > 0 && ClassUtils.isImplementationOf(Parameter(initBean.parameters[0]).type.clazz, ICtx))
                {
                    args.push(this);
                }
                initBean.invoke(instance, args);
            }
            if (instance is IEventDispatcher)
            {
                IEventDispatcher(instance).dispatchEvent(new Event(Event.INIT));
            }
            var aopEvent:AOPEvent = new AOPEvent(AOPEvent.BEAN_INITIALISED);
            aopEvent.beanId = id;
            dispatchEvent(aopEvent);
            return instance;
        }

        public function register(clazzOrInstance:Object, id:String = null):*
        {
            if (clazzOrInstance is Class)
            {
                return registerClass(clazzOrInstance as Class, id);
            }
            else
            {
                return registerInstance(clazzOrInstance, id);
            }
        }

        public function registerInstance(instance:*, id:String = null):*
        {
            if (!id)
            {
                id = String(instance);
            }
            beans[id] = instance;
            wire(instance);
            init(instance, id);
            beanRegistered(id);
            return instance;
        }

        public function registerClass(clazz:Class, id:String = null):*
        {
            if (!id)
            {
                id = String(clazz);
            }
            beanClasses[id] = clazz;
            beanRegistered(id);
            return id;
        }

        private function beanRegistered(id:String):void
        {
            var aopEvent:AOPEvent = new AOPEvent(AOPEvent.BEAN_REGISTERED);
            aopEvent.beanId = id;
            dispatchEvent(aopEvent);
            var pce:PropertyChangeEvent = new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, PropertyChangeEventKind.UPDATE, id);
            dispatchEvent(pce);
        }

        public function wire(instance:*):*
        {
            if (isSimpleOrSpecial(instance))
                return instance;
            var type:Type = Type.forInstance(instance);
            for each (var field:Field in type.fields)
            {
                wireField(field, instance);
            }
            return instance;
        }

        private function isSimpleOrSpecial(instance:*):Boolean
        {
            if (ObjectUtils.isSimple(instance) || instance is ICtx)
                return true;
            return false;
        }

        private function wireField(field:Field, instance:*):void
        {
            if (field is Accessor && !Accessor(field).isWriteable())
                return;
            var fieldId:String = field.name;
            if (excludedProperties.indexOf(fieldId) != -1)
                return;
            var bean:* = getBean(fieldId);
            if (bean)
            {
                instance[fieldId] = bean;
                return;
            }
            if (fieldId.indexOf("$") == 0)
            {
                var clazz:Class = field.type.clazz;
                var ids:Array = getBeansByType(clazz);
                if (!ids.length)
                {
                    throw new Error("Cannot find bean for " + fieldId + " of type " + clazz);
                }
                instance[fieldId] = getBean(ids[0]);
            }
        }

        public function hasBean(id:String):Boolean
        {
            return id in beans || id in beanClasses;
        }
    }
}